// ****************************************************************
// Copyright 2012, Stephan Burguchev
// e-mail: me@sburg.net
// ****************************************************************
// *
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using System.Security.AccessControl;
using System.Security.Principal;

namespace Updater.Utils
{
    public static class FileSystem
    {
        private static readonly string _currentUserSid;
        private static readonly List<string> _groupSids;

        static FileSystem()
        {
            //// It is possible to use System.DirectoryServices.AccountManagement to determine user groups
            //// but it is too slow. Uncomment the following line to use it.
            //// _groupSids = UserPrincipal.Current.GetAuthorizationGroups().Select(g => g.Sid.Value).ToList();

            try
            {
                string upn = getProcessOwnerUpn();

                WindowsIdentity identity;
                if (upn != null)
                {
                    try
                    {
                        identity = new WindowsIdentity(upn);
                    }
                    catch (Exception)
                    {
                        identity = WindowsIdentity.GetCurrent();
                    }
                }
                else
                {
                    identity = WindowsIdentity.GetCurrent();
                }

                if (identity != null)
                {
                    if (identity.Groups != null)
                    {
                        _groupSids = identity.Groups.Select(g => g.Value).ToList();
                    }

                    if (identity.User != null)
                    {
                        _currentUserSid = identity.User.Value;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        /// <summary>
        ///     Checks for full control on directory.
        /// </summary>
        /// <param name="dir">The dir.</param>
        /// <param name="fileSystemRightsToCheck"> </param>
        /// <param name="includingDescendants">
        ///     if set to <c>true</c> [including descendants].
        /// </param>
        /// <returns>
        ///     <c>true</c> if the application has full control over the directory (and descendant objects if it is checked).
        /// </returns>
        public static bool CheckForFileSystemRightsOnDirectory(
            string dir,
            FileSystemRights fileSystemRightsToCheck,
            bool includingDescendants = false)
        {
            if (dir == null)
            {
                throw new ArgumentNullException("dir");
            }

            if (_groupSids == null || _currentUserSid == null)
            {
                return false;
            }
#if DEBUG
            if (!Directory.Exists(dir))
            {
                throw new DirectoryNotFoundException("The directory provided does not exist.");
            }
#endif

            AuthorizationRuleCollection rules = Directory.GetAccessControl(dir).
                                                          GetAccessRules(true, true, typeof(SecurityIdentifier));
            bool theDirectoryIsFullyControlled = checkForFileSystemRights(rules, fileSystemRightsToCheck);

            if (!includingDescendants || !theDirectoryIsFullyControlled)
            {
                return theDirectoryIsFullyControlled;
            }

            // Checking the descendants
            bool theFilesIsFullyControlled =
                Directory.EnumerateFiles(dir).All(file => CheckForFullControlOnFile(file, fileSystemRightsToCheck));
            if (!theFilesIsFullyControlled)
            {
                return false;
            }

            return Directory.EnumerateDirectories(dir).All(
                d => CheckForFileSystemRightsOnDirectory(d, fileSystemRightsToCheck, true));
        }

        public static bool CheckForFullControlOnFile(string file, FileSystemRights fileSystemRightsToCheck)
        {
            if (file == null)
            {
                throw new ArgumentNullException("file");
            }

            if (_groupSids == null || _currentUserSid == null)
            {
                return false;
            }
#if DEBUG
            if (!File.Exists(file))
            {
                throw new FileNotFoundException("The filename provided does not exist.");
            }
#endif

            AuthorizationRuleCollection rules = File.GetAccessControl(file).
                                                     GetAccessRules(true, true, typeof(SecurityIdentifier));
            return checkForFileSystemRights(rules, fileSystemRightsToCheck);
        }

        public static void CopyDirectory(string sourceDir, string targetDir, bool overwrite)
        {
            if (sourceDir == null)
            {
                throw new ArgumentNullException("sourceDir");
            }

            if (targetDir == null)
            {
                throw new ArgumentNullException("targetDir");
            }

            foreach (string file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.TopDirectoryOnly))
            {
                File.Copy(file, Path.Combine(targetDir, Path.GetFileName(file)), overwrite);
            }

            foreach (string source in Directory.EnumerateDirectories(sourceDir, "*", SearchOption.TopDirectoryOnly))
            {
                string dirName = Path.GetFileName(source);
                string target = Path.Combine(targetDir, dirName);
                Directory.CreateDirectory(target);
                CopyDirectory(source, target, overwrite);
            }
        }

        private static bool checkForFileSystemRights(
            AuthorizationRuleCollection rules,
            FileSystemRights fileSystemRightsToCheck)
        {
            bool allow = false;
            bool deny = false;
            foreach (FileSystemAccessRule rule in rules)
            {
                if (rule.AccessControlType == AccessControlType.Deny &&
                    (rule.FileSystemRights & fileSystemRightsToCheck) == fileSystemRightsToCheck &&
                    (_groupSids.Contains(rule.IdentityReference.Value) || rule.IdentityReference.Value == _currentUserSid))
                {
                    deny = true;
                }

                if (rule.AccessControlType == AccessControlType.Allow &&
                    (rule.FileSystemRights & fileSystemRightsToCheck) == fileSystemRightsToCheck &&
                    (_groupSids.Contains(rule.IdentityReference.Value) || rule.IdentityReference.Value == _currentUserSid))
                {
                    allow = true;
                }
            }

            // If we have both allow and deny permissions, the deny takes precedent.
            return allow && !deny;
        }

        private static string getProcessOwnerUpn()
        {
            int pid = Process.GetCurrentProcess().Id;

            // The call to InvokeMethod below will fail if the Handle property is not retrieved
            var processQuery = new SelectQuery(
                "Win32_Process", string.Format("Handle = {0}", pid), new[] { "Handle", "ProcessId" });

            using (var searcher = new ManagementObjectSearcher(processQuery))
            {
                using (ManagementObjectCollection processes = searcher.Get())
                {
                    foreach (ManagementObject process in processes)
                    {
                        var outParameters = new object[2];
                        var result = (uint)process.InvokeMethod("GetOwner", outParameters);

                        if (result == 0)
                        {
                            var user = (string)outParameters[0];
                            var domain = (string)outParameters[1];
                            //// var processId = (uint)process["ProcessId"];

                            return string.Format("{0}@{1}", user, domain);
                        }
                    }

                    return null;
                }
            }
        }
    }
}
